package com.liuqianfei.markme;

import com.liuqianfei.linkify.LinkifyIt;
import com.liuqianfei.markme.core.Configuration;
import com.liuqianfei.markme.core.MarkMeException;
import com.liuqianfei.markme.core.Token;
import com.liuqianfei.markme.helpers.MarkMeHelper;
import com.liuqianfei.markme.presets.Default;
import com.liuqianfei.markme.rules.Environment;
import com.liuqianfei.markme.rules.StateCore;
import com.liuqianfei.mdurl.Decode;
import com.liuqianfei.mdurl.Encode;
import com.liuqianfei.mdurl.Format;
import com.liuqianfei.mdurl.MdUrl;
import com.liuqianfei.util.ArrayUtil;
import com.liuqianfei.util.RegExpUtil;
import com.liuqianfei.util.StringUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Main parser/renderer class.
 * https://markdown-it.github.io/markdown-it
 * @author liuqianfei
 **/
public class MarkMeParser
{
    /**
     * It provides named presets as a convenience to quickly enable/disable
     * active syntax rules and options for common use cases.
     * 
     * <dl>
     *  <dt>commonmark</dt>
     *  <dd>configures parser to strict <a href="http://commonmark.org/">CommonMark</a> mode.</dd>
     *  <dt>default</dt>
     *  <dd>similar to GFM, used when no preset name given. Enables all available rules,
     *  but still without html, typographer & autolinker.</dd>
     *  <dt>zero</dt>
     *  <dd>all rules disabled. Useful to quickly setup your config via `.enable()`.
     *  For example, when you need only `bold` and `italic` markup and nothing else.</dd>
     * </dl>
     */
    final String presetName = "default";
    
    public final Configuration configuration = new Default();
    
    /**
     * MarkdownIt#inline -> ParserInline
     *
     * <p>Instance of {@link ParserInline}. You may need it to add new rules when
     * writing plugins. For simple rules control use [[MarkdownIt.disable]] and
     * [[MarkdownIt.enable]].
     **/
    public ParserInline parserInline = new ParserInline();
    
    /**
     * MarkdownIt#block -> ParserBlock
     *
     * <p>Instance of {@link ParserBlock}. You may need it to add new rules when
     * writing plugins. For simple rules control use [[MarkdownIt.disable]] and
     * [[MarkdownIt.enable]].
     **/
    public ParserBlock parserBlock = new ParserBlock();
    
    /**
     * MarkdownIt#parserCore -> ParserCore
     *
     * <p>Instance of {@link ParserCore} chain executor. You may need it to add new rules when
     * writing plugins. For simple rules control use [[MarkdownIt.disable]] and
     * [[MarkdownIt.enable]].
     **/
    public ParserCore parserCore = new ParserCore();
    
    /**
     * MarkdownIt#renderer -> Renderer
     *
     * <p>Instance of {@link Renderer}. Use it to modify output look. Or to add rendering
     * rules for new token types, generated by plugins.
     **/
    Renderer renderer = new Renderer();
    
    /**
     * MarkdownIt#linkify -> LinkifyIt
     *
     * <p>{@link LinkifyIt} instance.
     **/
    LinkifyIt linkifyIt = new LinkifyIt();
    
    public MarkMeHelper markMeHelper = new MarkMeHelper();
    
    public MarkMeParser()
    {
        
    }

    ////////////////////////////////////////////////////////////////////////////////
    //
    // This validator can prohibit more than really needed to prevent XSS. It's a
    // tradeoff to keep code simple and to be secure by default.
    //
    // If you need different setup - override validator method as you wish. Or
    // replace it with dummy function and use external sanitizer.
    //

    String BAD_PROTO_RE = "^(vbscript|javascript|file|data):";
    String GOOD_DATA_RE = "^data:image\\/(gif|png|jpeg|webp);";

    
    public boolean validateLink(String url)
    {
        // url should be normalized at this point, and existing entities are decoded
        String str = url.trim().toLowerCase();
        return !RegExpUtil.match(BAD_PROTO_RE, str, false) || (RegExpUtil.match(GOOD_DATA_RE, str, false));
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    
    String[] RECODE_HOSTNAME_FOR = {"http:", "https:", "mailto:"};

    MdUrl mdUrl = new MdUrl();
    Format mdFormat = new Format();
    Encode mdEncode = new Encode();
    Decode mdDecode = new Decode();

    public String normalizeLink(String url)
    {
        MdUrl parsed = mdUrl.parse(url, true);

        if (StringUtil.isNotEmpty(parsed.hostname))
        {
            // Encode hostnames in urls like:
            // `http://host/`, `https://host/`, `mailto:user@host`, `//host/`
            //
            // We don't encode unknown schemas, because it's likely that we encode
            // something we shouldn't (e.g. `skype:name` treated as `skype:host`)
            //
            if (StringUtil.isEmpty(parsed.protocol) || ArrayUtil.indexOf(RECODE_HOSTNAME_FOR, parsed.protocol) >= 0)
            {
                try
                {
                    parsed.hostname = StringUtil.toAscii(parsed.hostname);
                }
                catch (Exception e)
                {
                    /**/
                }
            }
        }
        return mdEncode.encode(mdFormat.format(parsed), null, true);
    }
    
    public String normalizeLinkText(String url)
    {
        MdUrl parsed = mdUrl.parse(url, true);

        if (StringUtil.isNotEmpty(parsed.hostname))
        {
            // Encode hostnames in urls like:
            // `http://host/`, `https://host/`, `mailto:user@host`, `//host/`
            //
            // We don't encode unknown schemas, because it's likely that we encode
            // something we shouldn't (e.g. `skype:name` treated as `skype:host`)
            //
            if (StringUtil.isEmpty(parsed.protocol) || ArrayUtil.indexOf(RECODE_HOSTNAME_FOR, parsed.protocol) >= 0)
            {
                try
                {
                    parsed.hostname = StringUtil.toUnicode(parsed.hostname);
                }
                catch (Exception e)
                { 
                    /**/
                }
            }
        }
        return mdDecode.decode(mdFormat.format(parsed), null);
    }
    
    /** 
     * chainable
     *
     * <p>Set parser options (in the same format as in constructor). Probably, you
     * will never need it, but you can change options after constructor call.
     *
     * <p><strong>Note:</strong> To achieve the best possible performance, don't modify a
     * <code>MarkMeParser</code> instance options on the fly. If you need multiple configurations
     * it's best to create multiple instances and initialize each with separate config.
     **/
    MarkMeParser set(Configuration configuration)
    {
        return this;
    }


    /** chainable, internal
     * MarkdownIt.configure(presets)
     *
     * Batch load of all options and compenent settings. This is internal method,
     * and you probably will not need it. But if you with - see available presets
     * and data structure [here](https://github.com/markdown-it/markdown-it/tree/master/lib/presets)
     *
     * We strongly recommend to use presets instead of direct config loads. That
     * will give better compatibility with next versions.
     **/
    MarkMeParser configure(String presetName)
    {
        return this;
    }

    /** chainable
     * MarkdownIt.enable(list, ignoreInvalid)
     * - list (String|Array): rule name or list of rule names to enable
     * - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found.
     *
     * Enable list or rules. It will automatically find appropriate components,
     * containing rules with given names. If rule not found, and `ignoreInvalid`
     * not set - throws exception.
     *
     * ##### Example
     *
     * ```javascript
     * var md = require('markdown-it')()
     *             .enable(['sub', 'sup'])
     *             .disable('smartquotes');
     * ```
     **/
    MarkMeParser enable(List<String> enableList, boolean ignoreInvalid) throws MarkMeException
    {
        List<String> result = new ArrayList<>();
        List<String> enables;

        IParser[] parsers = {parserCore, parserBlock, parserInline};

        for (IParser parser : parsers)
        {
            enables = parser.getRuler().enable(enableList, true);
            result.addAll(enables);
        }

        enables = parserInline.ruler2.enable(enableList, true);
        result.addAll(enables);

        List<String> missed = enableList.stream().filter(rule -> !result.contains(rule)).collect(Collectors.toList());

        if (missed.size() > 0 && !ignoreInvalid)
        {
            throw new MarkMeException("MarkMeParser. Failed to enable unknown rule(s): " + missed);
        }

        return this;
    }

    /** chainable
     * MarkdownIt.disable(list, ignoreInvalid)
     * - list (String|Array): rule name or list of rule names to disable.
     * - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found.
     *
     * The same as [[MarkdownIt.enable]], but turn specified rules off.
     **/
    MarkMeParser disable(List<String> disableList, boolean ignoreInvalid) throws MarkMeException
    {
        List<String> result = new ArrayList<>();
        List<String> disables;

        IParser[] parsers = {parserCore, parserBlock, parserInline};

        for (IParser parser : parsers)
        {
            disables = parser.getRuler().disable(disableList, true);
            result.addAll(disables);
        }

        disables = parserInline.ruler2.disable(disableList, true);
        result.addAll(disables);

        List<String> missed = disableList.stream().filter(rule -> !result.contains(rule)).collect(Collectors.toList());

        if (missed.size() > 0 && !ignoreInvalid)
        {
            throw new MarkMeException("MarkMeParser. Failed to disable unknown rule(s): " + missed);
        }

        return this;
    }

    /** chainable
     * MarkdownIt.use(plugin, params)
     *
     * Load specified plugin with given params into current parser instance.
     * It's just a sugar to call `plugin(md, params)` with curring.
     *
     * ##### Example
     *
     * ```javascript
     * var iterator = require('markdown-it-for-inline');
     * var md = require('markdown-it')()
     *             .use(iterator, 'foo_replace', 'text', function (tokens, idx) {
     *               tokens[idx].content = tokens[idx].content.replace(/foo/g, 'bar');
     *             });
     * ```
     **/
    MarkMeParser use(Object plugin, String... parameters)
    {
//        plugin.apply(plugin, parameters);
        return this;
    }
    
    /** internal
     * MarkdownIt.parse(src, env) -> Array
     * - src (String): source string
     * - env (Object): environment sandbox
     *
     * Parse input string and returns list of block tokens (special token type
     * "inline" will contain list of inline tokens). You should not call this
     * method directly, until you write custom renderer (for example, to produce
     * AST).
     *
     * `env` is used to pass data between "distributed" rules and return additional
     * metadata like reference info, needed for the renderer. It also can be used to
     * inject data in specific cases. Usually, you will be ok to pass `{}`,
     * and then pass updated object to renderer.
     **/
    public List<Token> parse(String src, Environment env)
    {
        StateCore stateCore = new StateCore(src, this, env);
        this.parserCore.process(stateCore);
        return stateCore.tokens;
    }
    
    /**
     * MarkdownIt.render(src [, env]) -> String
     * - src (String): source string
     * - env (Object): environment sandbox
     *
     * Render markdown string into html. It does all magic for you :).
     *
     * `env` can be used to inject additional metadata (`{}` by default).
     * But you will not need it with high probability. See also comment
     * in [[MarkdownIt.parse]].
     **/
    public String render(String src, Environment env)
    {
        src = src.replaceAll(System.getProperty("line.separator"), String.valueOf('\n'));
        return this.renderer.render(this.parse(src, env), this.configuration, env);
    }

    public String render(String src)
    {
        return render(src, null);
    }
    
    /** internal
     * MarkdownIt.parseInline(src, env) -> Array
     * - src (String): source string
     * - env (Object): environment sandbox
     *
     * The same as [[MarkdownIt.parse]] but skip all block rules. It returns the
     * block tokens list with the single `inline` element, containing parsed inline
     * tokens in `children` property. Also updates `env` object.
     **/
    public List<Token> parseInline(String src, Environment env)
    {
        StateCore stateCore = new StateCore(src, this, env);
        stateCore.inlineMode = true;
        this.parserCore.process(stateCore);
        return stateCore.tokens;
    }


    /**
     * MarkdownIt.renderInline(src [, env]) -> String
     * - src (String): source string
     * - env (Object): environment sandbox
     *
     * Similar to [[MarkdownIt.render]] but for single paragraph content. Result
     * will NOT be wrapped into `<p>` tags.
     **/
    public String renderInline(String src, Environment env)
    {
        return this.renderer.render(this.parseInline(src, env), this.configuration, env);
    }
}